在计算器中的异常

  有了基本的异常处理机制,我们现在可以重做6.1节计算器的例子,将运行中所发现的错误的处理从计算器的基本逻辑中分离出来。这将导致一种程序组织结构,它更真实地反映了那些由互相分离的联系松散的部分构造出的程序的情况。

  首先,可以删除error()。与此对应,分析器函数只知道发出错误信号所使用的类型:

    namespace Error {
        struct Zero_divide { };

        struct Syntax_error {
            const char* p;
            Syntax_error(const char* q) { p = q; }
        };
    }

分析器检查三种语法错误

    Lexer::Token_value Lexer::get_token()
    {
        using namespace std;            // 使用输入,isalpha()等(6.1.7节)
            // ...
            default:                    // NAME, NAME =, 或者错误
                if(isalpha(ch)) {
                    string_value = ch;
                    while(input->get(ch) && isalnum(ch)) string_value.push_back(ch);
                    input->putback(ch);
                    return curr_tok = NAME;
                }
                throw Error::Syntax_error("bad token");
    }

    double Parser::prim(bool get)        // 处理初等项
    {
        // ...
        case Lexer::LP:
        {
            double e = expr(true);
            if(curr_tok != Lexer::RP) throw Error::Syntax_error("')' expected");
            get_token();        // 吃掉 ')'
            return e;
        }
        case Lexer::END:
            return 1;
        default:
            throw Error::Syntax_error("primary expected");
    }

在检查一到一个语法错误时,代码就通过throw将控制传递到在某个(直接或间接的)调用者里定义的异常处理器去。throw运算符同时给处理器送去一个值。例如,

throw Syntax_error("primary expected");

将向处理器传递一个Syntax_error对象,其中包含着一个指向字符串“primary expected”的指针。这正是那个处理器所期望的。

  报告除零错误不需要传递任何数据:

    double Parser::term(bool get)        // 乘和除
    {
        // ...
        case Lexer::DIV:
            if(double d = prim(true)) {
                left /= d;
                break;
            }
            throw Error::Zero_divide();
        // ...
    }

现在定义驱动程序,使之能处理Zero_divide和Syntax_error异常。比如写:

    int main(int argc, char* argv[])
    {
        // ...
        while(*input) {
            try {
                Lexer::get_token();
                if(Lexer::curr_tok == Lexer::END) break;
                if(Lexer::curr_tok == Lexer::PRINT) continue;
                cout << Parser::expr(false) << '\n';
            }
            catch(Error::Zero_divide) {
                cerr << "attempt to divide by zero\n";
                if(Lexer::curr_tok != Lexer::PRINT) skip();
            }
            catch(Error::Syntax_error e)
            {
                cerr << "syntax error:" << e.p << '\n';
                if(Lexer::curr_tok != Lexer::PRINT) skip();
            }
        }
        if(input != &cin) delete input;
        return no_of_errors;
    }

除非是在表达式的最后,由表示结束的PRINT单词(即,分号或者换行符)引起的错误,否则main()将调用恢复函数skip()。函数skip()尝试着将分析器带入一个定义良好的状态,它采用的方式就是丢掉一系列字符,直到遇到一个换行符或者分号为止。函数skip()、no_of_errors和inpnt是名字空间Driver的当然候选成员:

    namespace Driver {
        int no_of_errors;
        std::istream* input;
        void skip();
    }

    void Driver::skip()
    {
        no_of_errors++;
        while(*input) {        // 丢掉一些字符,直至遇到换行符或者分号
            char ch;
            input->get(ch);
            switch(ch) {
            case '\n':
            case ';':
                return;
            }
        }
    }

skip()的代码的抽象层次比分析器代码更低,这样做是有意的,是为了避免它在处理由分析器报告的异常期间被来自分析器的异常套住。

  我保留了统计出错次数并将这个数通过程序返回值报告的想法。知道一个程序是否遇到错误,它是否能够从错误中恢复等,这些都是非常有价值。

  我没有将main放进Driver名字空间。全局的main是整个程序的初启函数(3.2节),出现在某个名字空间里的main()就没有特殊意义了。在实际规模的程序中,应该将main()中的大部分代码移到Driver里的另一个独立函数里。

🔚